Passed
Pull Request — master (#17)
by
unknown
02:50
created

GraphHelpers.ts ➔ compareNodes   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
1
import { DependencyLink, DependencyNode, NodeSelection } from '../../components/types';
2
import { event, select, selectAll } from 'd3-selection';
3
import { Simulation } from 'd3-force';
4
import { drag } from 'd3-drag';
5
import { zoom, zoomIdentity } from 'd3-zoom';
6
import { BACKGROUND_HIGHLIGHT_OPACITY, BASE_FONT_SIZE, LabelColors, Selectors, TextColors, TRANSITION_DURATION } from '../AppConsts';
7
import { ZoomScaleStorage } from './UserEventHelpers';
8
import {
9
    selectAllLinks,
10
    selectAllNodes,
11
    selectDetailsButtonRect,
12
    selectDetailsButtonText,
13
    selectDetailsButtonWrapper,
14
    selectHighlightBackground,
15
    selectHighLightedNodes,
16
} from './Selectors';
17
18
export function getLabelTextDimensions(node: Node) {
19
    const textNode = select<SVGGElement, DependencyNode>(node.previousSibling as SVGGElement).node();
20
21
    if (!textNode) {
22
        return undefined;
23
    }
24
25
    return textNode.getBBox();
26
}
27
28
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
29
    const foundNode = select<SVGGElement, DependencyNode>('#labels')
30
        .selectAll<SVGGElement, DependencyNode>('g')
31
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
32
        .node();
33
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
34
}
35
36
export function findMaxDependencyLevel(labelNodesGroup: NodeSelection<SVGGElement>) {
37
    return (
38
        Math.max(
39
            ...labelNodesGroup
40
                .selectAll<HTMLElement, DependencyNode>('g')
41
                .filter((node: DependencyNode) => node.level > 0)
42
                .data()
43
                .map((node: DependencyNode) => node.level)
44
        ) - 1
45
    );
46
}
47
48
export function highlight(clickedNode: DependencyNode) {
49
    const linksData = selectAllLinks().data();
50
    const labelNodes = selectAllNodes();
51
52
    const visitedNodes = setDependencyLevelOnEachNode(clickedNode, labelNodes.data());
53
54
    if (visitedNodes.length === 1) {
55
        return;
56
    }
57
58
    labelNodes.each(function(this: SVGGElement, node: DependencyNode) {
59
        const areNodesDirectlyConnected = areNodesConnected(clickedNode, node, linksData);
60
        const labelElement = this.firstElementChild;
61
        const textElement = this.lastElementChild;
62
63
        if (!labelElement || !textElement) {
64
            return;
65
        }
66
67
        if (areNodesDirectlyConnected) {
68
            select<Element, DependencyNode>(labelElement).attr('fill', getHighLightedLabelColor);
69
            select<Element, DependencyNode>(textElement).style('fill', TextColors.HIGHLIGHTED);
70
        } else {
71
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
72
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
73
        }
74
    });
75
}
76
77
export function centerScreenToDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale?: number) {
78
    if (!dimension) {
79
        return;
80
    }
81
    const svgContainer = select(Selectors.CONTAINER);
82
83
    const width = Number(svgContainer.attr('width'));
84
    const height = Number(svgContainer.attr('height'));
85
86
    const scaleValue = scale || Math.min(1.3, 0.9 / Math.max(dimension.width / width, dimension.height / height));
87
88
    ZoomScaleStorage.setScale(scaleValue);
89
    svgContainer
90
        .transition()
91
        .duration(TRANSITION_DURATION)
92
        .call(
93
            zoom<any, any>().on('zoom', changeZoom(Selectors.ZOOM_OVERVIEW)).transform,
94
            zoomIdentity
95
                .translate(width / 2, height / 2)
96
                .scale(scaleValue)
97
                .translate(-dimension.x - dimension.width / 2, -dimension.y - dimension.height / 2)
98
        );
99
}
100
101
export function hideHighlightBackground() {
102
    const detailsButtonRectSelection = selectDetailsButtonRect();
103
    const detailsButtonTextSelection = selectDetailsButtonText();
104
    selectAll([selectHighlightBackground().node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
105
        .transition()
106
        .duration(TRANSITION_DURATION)
107
        .style('opacity', 0)
108
        .end()
109
        .then(() => {
110
            selectDetailsButtonWrapper().lower();
111
        });
112
}
113
114
function showHighlightBackground(dimension: ReturnType<typeof findGroupBackgroundDimension>) {
115
    if (!dimension) {
116
        return;
117
    }
118
    const highlightBackground = selectHighlightBackground();
119
    const detailsButtonRectSelection = selectDetailsButtonRect();
120
    const detailsButtonTextSelection = selectDetailsButtonText();
121
122
    const scaleValue = ZoomScaleStorage.getScale();
123
124
    const isBackgroundActive = highlightBackground.style('opacity') === String(BACKGROUND_HIGHLIGHT_OPACITY);
125
126
    const scaleMultiplier = 1 / scaleValue;
127
128
    const buttonWidth = 100 * scaleMultiplier;
129
    const buttonHeight = 60 * scaleMultiplier;
130
    const buttonMarginBottom = 10 * scaleMultiplier;
131
    const buttonMarginRight = 40 * scaleMultiplier;
132
    const buttonX = dimension.x + dimension.width - buttonWidth - buttonMarginRight;
133
    const buttonY = dimension.y + dimension.height - buttonHeight - buttonMarginBottom;
134
    const buttonRadius = 5 * scaleMultiplier;
135
    const buttonTextFontSize = BASE_FONT_SIZE * scaleMultiplier;
136
    const buttonTextPositionX = dimension.x + dimension.width - buttonWidth / 2 - buttonMarginRight;
137
    const buttonTextPositionY = dimension.y + dimension.height - buttonHeight / 2 + 6 * scaleMultiplier - buttonMarginBottom;
138
139
    const elementsNextAttributes = [
140
        {
141
            x: dimension.x,
142
            y: dimension.y,
143
            width: dimension.width,
144
            height: dimension.height,
145
            opacity: BACKGROUND_HIGHLIGHT_OPACITY,
146
        },
147
        {
148
            x: buttonX,
149
            y: buttonY,
150
            rx: buttonRadius,
151
            ry: buttonRadius,
152
            width: buttonWidth,
153
            height: buttonHeight,
154
            opacity: 1,
155
        },
156
        {
157
            fontSize: buttonTextFontSize,
158
            x: buttonTextPositionX,
159
            y: buttonTextPositionY,
160
            opacity: 1,
161
        },
162
    ];
163
164
    if (isBackgroundActive) {
165
        selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
166
            .data(elementsNextAttributes)
167
            .transition()
168
            .duration(TRANSITION_DURATION)
169
            .attr('x', data => data.x)
170
            .attr('y', data => data.y)
171
            .attr('rx', data => data.rx || 0)
172
            .attr('ry', data => data.ry || 0)
173
            .attr('width', data => data.width || 0)
174
            .attr('height', data => data.height || 0)
175
            .attr('font-size', data => data.fontSize || 0);
176
    } else {
177
        selectDetailsButtonWrapper().raise();
178
        selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
179
            .data(elementsNextAttributes)
180
            .attr('x', data => data.x)
181
            .attr('y', data => data.y)
182
            .attr('rx', data => data.rx || 0)
183
            .attr('ry', data => data.ry || 0)
184
            .attr('width', data => data.width || 0)
185
            .attr('height', data => data.height || 0)
186
            .attr('font-size', data => data.fontSize || 0)
187
            .transition()
188
            .duration(TRANSITION_DURATION)
189
            .style('opacity', data => data.opacity);
190
    }
191
}
192
193
export function zoomToHighLightedNodes() {
194
    const highlightedNodes = selectHighLightedNodes();
195
    const dimension = findGroupBackgroundDimension(highlightedNodes.data());
196
197
    centerScreenToDimension(dimension);
198
    showHighlightBackground(dimension);
199
}
200
201
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
202
    nodes.forEach((node: DependencyNode) => (node.level = 0));
203
204
    const visitedNodes: DependencyNode[] = [];
205
    const nodesToVisit: DependencyNode[] = [];
206
207
    clickedNode.level = 1;
208
209
    nodesToVisit.push(clickedNode);
210
211
    while (nodesToVisit.length > 0) {
212
        const currentNode = nodesToVisit.shift();
213
214
        if (!currentNode) {
215
            return [];
216
        }
217
218
        currentNode.links.forEach((node: DependencyNode) => {
219
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
220
                node.level = currentNode.level + 1;
221
                nodesToVisit.push(node);
222
            }
223
        });
224
225
        visitedNodes.push(currentNode);
226
    }
227
228
    return visitedNodes;
229
}
230
231
function containsNode(arr: DependencyNode[], node: DependencyNode) {
232
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
233
}
234
235
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
236
    node1: T,
237
    node2: K
238
): Boolean {
239
    return node1.name === node2.name && node1.version === node2.version;
240
}
241
242
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
243
    return (
244
        a.index === b.index ||
245
        links.some(
246
            link =>
247
                (link.source.index === a.index && link.target.index === b.index) ||
248
                (link.source.index === b.index && link.target.index === a.index)
249
        )
250
    );
251
}
252
253
export function getHighLightedLabelColor(node: DependencyNode) {
254
    const { isConsumer, isProvider } = node;
255
256
    if (isConsumer && isProvider) {
257
        return LabelColors.PROVIDER_CONSUMER;
258
    }
259
260
    if (isProvider) {
261
        return LabelColors.PROVIDER;
262
    }
263
264
    if (isConsumer) {
265
        return LabelColors.CONSUMER;
266
    }
267
268
    return LabelColors.DEFAULT;
269
}
270
271
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
272
    let isDragStarted = false;
273
    return drag<SVGGElement, DependencyNode>()
274
        .on('start', (node: DependencyNode) => {
275
            if (!selectHighLightedNodes().data().length) {
276
                dragStarted(node, simulation);
277
                isDragStarted = true;
278
            }
279
        })
280
        .on('drag', (node: DependencyNode) => {
281
            if (isDragStarted) {
282
                dragged(node);
283
            }
284
        })
285
        .on('end', (node: DependencyNode) => {
286
            dragEnded(node, simulation);
287
            isDragStarted = false;
288
        });
289
}
290
291
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
292
    if (!event.active) {
293
        simulation.alphaTarget(0.3).restart();
294
    }
295
    node.fx = node.x;
296
    node.fy = node.y;
297
}
298
299
function dragged(node: DependencyNode) {
300
    node.fx = event.x;
301
    node.fy = event.y;
302
}
303
304
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
305
    if (!event.active) {
306
        simulation.alphaTarget(0);
307
    }
308
    node.fx = null;
309
    node.fy = null;
310
}
311
312
export const changeZoom = (zoomSelector: Selectors.ZOOM_OVERVIEW | Selectors.ZOOM_DETAILS) => () => {
313
    const { transform } = event;
314
    const zoomLayer = select(zoomSelector);
315
    zoomLayer.attr('transform', transform);
316
    zoomLayer.attr('stroke-width', 1 / transform.k);
317
    ZoomScaleStorage.setScale(transform.k);
318
};
319
320
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
321
    if (nodesGroup.length === 0) {
322
        return;
323
    }
324
325
    let upperLimitNode = nodesGroup[0];
326
    let lowerLimitNode = nodesGroup[0];
327
    let leftLimitNode = nodesGroup[0];
328
    let rightLimitNode = nodesGroup[0];
329
330
    nodesGroup.forEach((node: DependencyNode) => {
331
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
332
            return;
333
        }
334
        if (node.x > rightLimitNode.x) {
335
            rightLimitNode = node;
336
        }
337
338
        if (node.x < leftLimitNode.x) {
339
            leftLimitNode = node;
340
        }
341
342
        if (node.y < upperLimitNode.y) {
343
            upperLimitNode = node;
344
        }
345
346
        if (node.y > lowerLimitNode.y) {
347
            lowerLimitNode = node;
348
        }
349
    });
350
351
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
352
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
353
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
354
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
355
356
    return {
357
        x: leftLimitWithOffset,
358
        y: upperLimitWithOffset,
359
        width,
360
        height,
361
    };
362
}
363